/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.skatiger.jrove.spring.form.renderer;

import java.beans.PropertyEditor;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;

import org.skatiger.jrove.JroveException;
import org.skatiger.jrove.ResponseWriter;
import org.skatiger.jrove.ViewContext;
import org.skatiger.jrove.component.UIComponent;
import org.skatiger.jrove.renderer.Html;
import org.skatiger.jrove.spring.utils.ValueFormatter;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.support.BindStatus;
import org.springframework.web.servlet.tags.form.TagWriter;

public class OptionsRenderer extends AbstractFormElementRenderer {
    protected static final String LABEL_FACET_NAME = "label";
    protected static final String ITEM_PROPERTY_NAME = "items";
    protected static final String ITEM_VALUE_PROPERTY_NAME = "itemValue";
    protected static final String VAR_PROPERTY_NAME = "var";

    /* (non-Javadoc)
     * @see org.skatiger.jrove.component.Renderer#encodeBegin(org.skatiger.jrove.ViewContext, org.skatiger.jrove.component.UIComponent)
     */
    @Override
    public void encodeBegin(ViewContext context, UIComponent component)
            throws IOException {
        Object items = component.getAttributes().get(ITEM_PROPERTY_NAME);
        Object itemsObject = null;
        BindStatus bindStatus = getBindStatus(component);
        Class<?> selectTagBoundType = bindStatus.getValueType();
        if (selectTagBoundType != null && selectTagBoundType.isEnum()) {
            itemsObject = selectTagBoundType.getEnumConstants();
        } else {
            itemsObject = items;
        }

        if (itemsObject != null) {
            if (itemsObject.getClass().isArray()) {
                renderFromArray(context, component, itemsObject);
            } else if (itemsObject instanceof Collection) {
                renderFromCollection(context, component, itemsObject);
            } else if (itemsObject instanceof Map) {
                renderFromMap(context, component, itemsObject);
            } else if (itemsObject instanceof Class
                    && ((Class) itemsObject).isEnum()) {
                renderFromEnum(context, component, itemsObject);
            } else {
                throw new JroveException("Type ["
                        + itemsObject.getClass().getName()
                        + "] is not valid for option items");
            }
        }
    }

    private void renderFromArray(ViewContext context, UIComponent component,
            Object optionSource) throws IOException {
        doRenderFromCollection(context, component,
                CollectionUtils.arrayToList(optionSource));
    }

    private void renderFromMap(ViewContext context, UIComponent component,
            Object optionSource) throws IOException {
        Map<?, ?> optionMap = (Map) optionSource;
        String valueProperty = getValueProperty(component);
        for (Map.Entry entry : optionMap.entrySet()) {
            Object mapKey = entry.getKey();
            Object mapValue = entry.getValue();
            Object renderValue = (StringUtils.hasText(valueProperty) ? PropertyAccessorFactory
                    .forBeanPropertyAccess(mapKey).getPropertyValue(valueProperty) : mapKey);
            renderOption(context, component, mapKey, renderValue);
        }
    }

    private String getValueProperty(UIComponent component) {
        String itemValue = (String) component.getAttributes().get(
                ITEM_VALUE_PROPERTY_NAME);
        return (itemValue != null ? ObjectUtils.getDisplayString(itemValue)
                : null);
    }

    private void renderFromCollection(ViewContext context,
            UIComponent component, Object optionSource) throws IOException {
        doRenderFromCollection(context, component, (Collection) optionSource);
    }

    /**
     * Renders the inner '<code>option</code>' tags using the
     * {@link #optionSource}.
     * 
     * @see #doRenderFromCollection(java.util.Collection, TagWriter)
     */
    private void renderFromEnum(ViewContext context, UIComponent component,
            Object optionSource) throws IOException {
        doRenderFromCollection(context, component,
                CollectionUtils.arrayToList(((Class) optionSource)
                        .getEnumConstants()));
    }

    /**
     * Renders the inner '<code>option</code>' tags using the supplied
     * {@link Collection} of objects as the source. The value of the
     * {@link #valueProperty} field is used when rendering the '
     * <code>value</code>' of the '<code>option</code>' and the value of the
     * {@link #labelProperty} property is used when rendering the label.
     */
    private void doRenderFromCollection(ViewContext context,
            UIComponent component, Collection optionCollection)
            throws IOException {
        String valueProperty = getValueProperty(component);
        for (Object item : optionCollection) {
            BeanWrapper wrapper = PropertyAccessorFactory
                    .forBeanPropertyAccess(item);
            Object value;
            if (StringUtils.hasText(valueProperty)) {
                value = wrapper.getPropertyValue(valueProperty);
            } else if (item instanceof Enum) {
                value = ((Enum<?>) item).name();
            } else {
                value = item;
            }
            renderOption(context, component, item, value);
        }
    }

    private String getDisplayString(BindStatus bindStatus, Object value,
            boolean isHtmlEscape) {
        PropertyEditor editor = (value != null ? bindStatus.findEditor(value
                .getClass()) : null);
        return ValueFormatter.getDisplayString(value, editor, isHtmlEscape);
    }

    /**
     * Determine whether the supplied values matched the selected value.
     * Delegates to {@link SelectedValueComparator#isSelected}.
     */
    private boolean isOptionSelected(BindStatus bindStatus, Object resolvedValue) {
        return SelectedValueComparator.isSelected(bindStatus, resolvedValue);
    }

    private void renderOption(ViewContext context, UIComponent component,
            Object item, Object value) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        writer.startElement(Html.OPTION_ELEM, null);
        BindStatus bindStatus = getBindStatus(component);
        String valueDisplayString = getDisplayString(bindStatus, value,
                isHtmlEscape(component));
        // allows render values to handle some strange browser compat issues.
        writer.writeAttribute(Html.VALUE_ATTR, valueDisplayString,
                Html.VALUE_ATTR);

        if (isOptionSelected(bindStatus, value)
                || (value != item && isOptionSelected(bindStatus, item))) {
            writer.writeAttribute(Html.SELECTED_ATTR, true, Html.SELECTED_ATTR);
        }
        Boolean disabled = (Boolean) component.getAttributes().get(
                Html.DISABLED_ATTR);
        if (disabled != null ? disabled : Boolean.FALSE) {
            writer.writeAttribute(Html.DISABLED_ATTR, true, Html.DISABLED_ATTR);
        }
        {//renderer option label 
            UIComponent labelFacet = component.getFacet(LABEL_FACET_NAME);// get label fact
            if (labelFacet != null) {
                String var = (String) component.getAttributes().get(
                        VAR_PROPERTY_NAME);
                Object previousValue = context.getAttribute(var);
                context.setAttribute(var, item);// set attribute

                labelFacet.encodeAll(context);//renderer label fact

                context.removeAttribute(var); // remove attribute
                if (previousValue != null) {
                    context.setAttribute(var, previousValue);
                }
                writer.writeText("", null);//close start option element
            } else {// if label facet is null, render item.toString() with option label
                writer.writeText(item.toString(), null);
            }
        }
        writer.endElement(Html.OPTION_ELEM);
    }

}
